/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package neuron.file;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
import java.util.StringTokenizer;

import neuron.Coord;
import neuron.Dendrite;
import neuron.Network;
import neuron.Neuron;
import neuron.Segment;
import neuron.Soma;
import neuron.Statistics;

/**
 *
 * @author risto
 *
 * Loader for NeuroLucida ASC files
 */
public class NeuroLucidaLoader {

    private StringBuffer contents = null;
    private List<List> root;
    private Stack<List> current = new Stack<List>();
    private Neuron neuron = null;


    public static Network load(File f)  throws FileNotFoundException, IOException, ParseException
    {
		 AuxData.openDB(f.getParentFile());
    	NeuroLucidaLoader ldr = new NeuroLucidaLoader(f);
    	Network net = new Network();
    	Neuron n = ldr.getNeuron();
    	n.setNetwork(net);
    	net.add(n);
    	net.setFile(f);
    	return net;
    }


    public NeuroLucidaLoader(File f) throws FileNotFoundException, IOException, ParseException
    {
        readContents(f); // Read file contents to buffer
        parseStructure(); // Parse structure
        makeNeuron(); // Make neuron from file contents
        neuron.addMetadata("FILE", f.getName());
    }

    

    /**
     *
     * Read file contents into a buffer. Remove comments & empty lines.
     *
     * @param f
     * @throws FileNotFoundException
     * @throws IOException
     */
    private void readContents(File f) throws FileNotFoundException, IOException
    {
        // load contents
        contents = new StringBuffer((int)f.length());

        BufferedReader br = new BufferedReader(new FileReader(f));
        while (true) {
            String line = br.readLine();
            if (line == null) break; // END OF FILE

            // remove comments
            int comStart = line.indexOf(";");
            if (comStart != -1) {
                line = line.substring(0, comStart);
            }
            line = line.trim();

            if (line.length() == 0) continue; // EMPTY LINE

            line = line.replaceAll("[\t\n\r ]", " "); // remove linefeeds

            contents.append(line + " ");
        }
    }

    /**
     *
     * Parse file content from buffer (see readContents).
     * 
     * The file is structured as lists. This method will coerce the contents 
     * to recursive Lists, with first List being in root. 
     * 
     * e.g.
     * 
     * ( Axon
     *   ( "White" )
     *   ( rgb 1.0 1.0 1.0 )
     *   ( 1 2 3 4 
     *     1 2 3 5
     *     1 2 3 6
     *     1 2 3 7
     *       ( 2 2 2 2    ;; branch
     *         2 3 2 3 
     *         2 4 2 4 
     *        )))  
     *
     * -> root = List { "Axon", List { "White" }, List { "rgb", 1.0, 1.0, 1.0 }, 
     *                  List {  List { 1 2 3 4 }, List { 1 2 3 5 } , ... }}
     */
    private void parseStructure()
    {
        // parse content
        root = new ArrayList<List>();
        current.push(root);

        StringTokenizer st = new StringTokenizer(contents.toString());

        while(st.hasMoreTokens()) {

            String token = st.nextToken();

            // special handling for strings (in filenames, mostly)
            if (token.startsWith("\"") && token.endsWith("\"")) {
                current.peek().add(token);
                continue;
            }

            // <( x y z w )> ; Spine  - just ignore
            if (token.startsWith("<")) { token = token.substring(1); }
            if (token.endsWith(">")) {
                token = token.substring(0, token.length()-1);
            }

            // Start of List - "(  .."
            if (token.startsWith("(")) {
                List newList = new ArrayList();
                current.peek().add(newList);
                current.push(newList);
                
                if (token.length() == 1) continue; // only "(" ?
                token = token.substring(1).trim(); // cut '(' and continue parsing
            }
            
            // End of List  ".. )" or ")" or ")))" 
            if (token.contains(")")) {
            	// handle all endings
                while (token.contains(")")) {
                	// Just ")"
                    if (token.startsWith(")")) {
                        current.pop(); // pop stack items i.e. return to upper level
                        token = token.substring(1).trim();
                    }
                    // Has data "xxx)"
                    else {
                    	// something before ')', so add it to current list
                        int idx = token.indexOf(")");
                        String t2 = token.substring(0, idx);
                        token = token.substring(idx).trim(); // remove content from token & continue
                        current.peek().add(t2);
                    }
                }
            } 
            
            // Data
            else {
                // just data
                current.peek().add(token);
            }
        }
    }

  
    /**
     *
     * Make neuron from file contents.
     *
     * @return
     * @throws ParseException
     */
    private void makeNeuron() 
    {
        neuron = new Neuron();
        
        // Root list of file
        for (List l : root) {
        	
        	// Detect list type from first item
            Object first = l.get(0);

            // ( "String" .. 
            if (first instanceof String) {
                String s = (String) l.get(0);

                // ( "ImageCoords" .. 
                if (s.equalsIgnoreCase("ImageCoords")) {
                    // Image Coords
                    neuron.addMetadata("ImageCoords", l);
                    continue;
                }
                // ( "CellBody" .. 
                else if (s.equalsIgnoreCase("\"CellBody\"")) {
                	try {
                		neuron.setSoma(parseCellBody(l));
                	} catch (ParseException pe) {
                		neuron.error(pe.getMessage());
                	}
                    continue;
                }
                // ( "SomethingElse" ..
                else {
                    neuron.addMetadata(s, l);
                    continue;
                }
            }

            //  ( (Color White)  (Axon) ( 1.0 1.0 1.0 1.0 ) ( 1.0 2.0 2.0 1.0 ) .. 
            if (first instanceof List) {
            	
            	// Get second item to determine type (Axon, Dendrite) 
                Object second = l.get(1);
                if (second instanceof List && ((List)second).size() == 1) {
                    String name = (String) ((List)second).get(0);
                    
                    // Check if axon, dendrite or apical
                    if (name.equalsIgnoreCase("Axon") || name.equalsIgnoreCase("Dendrite") || name.equalsIgnoreCase("Apical")) {
                        parseDendrite(neuron, l);
                    }
                    
                    else {
                        neuron.error("Can't understand list type: " + name);
                    }
                } else {
                	neuron.error("Unexpected structure near (" + first + ", " + second + " ... " );
                }
            }
        }
    }
    
    /**
     * Parse Soma from list l.
     * 
     * @param l
     * @return
     */
    public Soma parseCellBody(List l) throws ParseException
    {
        String tag = (String) l.get(0);
        List color = (List) l.get(1);
        List name = (List) l.get(2);
        List<Coord> points = new ArrayList<Coord>();
        for (int i = 3; i < l.size(); i++) {
            List l2 = (List) l.get(i);
            // coordinate list
            if (l2.size() == 4 && l2.get(0) instanceof String) {
                Coord c = new Coord((String)l2.get(0), (String)l2.get(1), (String)l2.get(2), (String)l2.get(3));
                points.add(c);
            } else {
                throw new ParseException("Error reading CellBody coordinates: " + l2, 0);
            }
        }
        return new Soma(points);

    }
    
    
    /**
    *
    * Parse dendrite data from list
    * 
    * ( (Color white) 
    *   (Axon)
    *   ( ( 1.0 1.0 1.0 1.0 )
    *     ( 2.0 2.0 2.0 1.0 )
    *     ( 3.0 3.0 3.0 3.0 )
    *     ( 
    *       ( 4.0 4.0 4.0 4.0 )
    *       ( 5.0 5.0 5.0 5.0 )
    *       )))
    *
    * @param n
    * @param l
    */
   private void parseDendrite(Neuron n, List l)
   {
   	// Color
       List color = (List) l.get(0);
       
       // Type (Axon|Dendrite|Apical)
       List type = (List) l.get(1);
       String className = (String) type.get(0);
       
       // Coordinates
       Segment tmp = new Segment();
       parseBranch(tmp, l.subList(2, l.size()));
       Segment seg = tmp.getBranch(0);

       if (className.equalsIgnoreCase("Axon")) {
           Dendrite ax = new Dendrite(seg);
           ax.addMetadata("Color", color);
           n.setAxon(ax);
       }
       else if (className.equalsIgnoreCase("Dendrite")) {
    	   Dendrite de = new Dendrite(seg);
    	   de.addMetadata("Color", color);
           n.addBasalDendrite(de);
       }
       else if (className.equalsIgnoreCase("Apical")) {
    	   Dendrite de = new Dendrite(seg);
    	   de.addMetadata("Color", color);
           n.setApicalDendrite(de);
           System.out.println("Apical set, length " + Statistics.from(seg).totalLength());
       }

//       double len = dendrite.countLength();
//       System.out.println(className + "[" + (String)color.get(1) + "], branches " + dendrite.countBranchesRec() + ", terminals " + dendrite.countTerminalsRec() + ", length " + len);
   }

   
   /**
   *
   * Parse dendrite branch data.
   * 
   *  ( (1.0 1.0 1.0 1.0)
   *    (1.0 2.0 2.0 1.0)
   *    ( ... )
   *    .. 
   *    )
   *
   * @param l
   * @return
   */
  private void parseBranch(Segment parent, List l)
  {
      Segment[] paths = new Segment[3];
      for (int i = 0; i < 3; i++) paths[i] = new Segment();
      
      int idx = 0;

      for (Object o : l) {
    	  
    	  // ( 1 2 3 4 )   or   (  (1 2 3 4) .. ) 
          if (o instanceof List) {
              List l2 = (List) o;

              // coordinate list ( 1.0 1.0 1.0 1.0 )
              if (l2.size() == 4 && l2.get(0) instanceof String) {
                  Coord c = new Coord((String)l2.get(0), (String)l2.get(1), (String)l2.get(2), (String)l2.get(3));
                  paths[idx].addPoint(c);
              }

              // branch list  (  ( coords ) ( coords ) .. )
              else {
                  parseBranch(paths[idx], l2);
              }
          }
          
          // "Normal"  "Incomplete"  "|" 
          else if (o instanceof String) {
              String s2 = (String) o;

              // termination type ?
              if (s2.equalsIgnoreCase("Normal") || s2.equalsIgnoreCase("Incomplete")) {
              }
              
              // branch marker
              else if (s2.equalsIgnoreCase("|")) {
                  // branch
                  idx++;
              }
          }
          
      }
      
      // add branches to parent
      for (int i = 0; i <= idx; i++) {
    	  parent.addBranch(paths[i]);
    	  paths[i].setParent(parent);
      }
  }



    /**
     * 
     * Get neuron representing the file contents.
     * 
     * @return
     */
    public Neuron getNeuron() {
        return neuron;
    }



}
